/**
 * @(#)StopWatch.java, 2018/1/4.
 * <p/>
 * Copyright 2018 Netease, Inc. All rights reserved.
 * NETEASE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.chengzhi.structure;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 计时器，用于统计程序运行时间
 */
public class StopWatch {

    private static final String TAG_PREFIX = "__StopWatch~";

    private static final String START_TAG = TAG_PREFIX + "start";

    private static final String END_TAG = TAG_PREFIX + "end";

    private static final AtomicInteger tagSeq = new AtomicInteger(1);

    private Map<String, TimePoint> timePointMap = new HashMap<>();

    private List<TimePoint> timePointList = new ArrayList<>();

    public static StopWatch newInstance() {
        return new StopWatch();
    }

    private StopWatch() {
        TimePoint start = TimePoint.newInstance(START_TAG, timePointList.size());
        timePointList.add(start);
        timePointMap.put(start.tag, start);
    }

    /**
     * 程序耗时统计辅助函数
     *
     * @param runnable 运行程序
     * @param callback 耗时回调
     */
    public static void run(Runnable runnable, StopWatchCallback callback) {
        StopWatch stopWatch = newInstance();
        try {
            runnable.run();
        } finally {
            callback.runtime(stopWatch.spendTotal());
        }
    }

    public static void await(CyclicBarrier barrier) {
        if (null != barrier) {
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 程序耗时统计辅助函数
     *
     * @param callable 运行程序
     * @param callback 耗时回调
     * @return 返回运行程序所返回数据
     */
    public static <T> T call(Callable<T> callable, StopWatchCallback callback) {
        StopWatch stopWatch = newInstance();
        try {
            return callable.call();
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            } else {
                throw new RuntimeException(e);
            }
        } finally {
            callback.runtime(stopWatch.spendTotal());
        }
    }

    /**
     * 设置时间点标记
     *
     * @return 时间点标记符号
     */
    public String mark() {
        String markTag = nextTag();
        markInternal(markTag);
        return markTag;
    }

    /**
     * 设置时间点标记
     *
     * @param tag 标记符号
     * @return 标记成功返回true，反之返回false
     */
    public boolean mark(String tag) {
        if (nonEmpty(tag) && !tag.startsWith(TAG_PREFIX)) {
            markInternal(tag);
            return true;
        } else {
            return false;
        }
    }

    /**
     * 获取上一次mark的时间点标记符
     *
     * @return 上一次mark的时间点标记符
     */
    public String lastMarkTag() {
        return tag(-1);
    }

    /**
     * 获取指定位置的时间点标记符号
     * 1. 支持正向反向索引
     * 2. 起始时间点为0
     * 3. 尾时间点为-1
     * 4. 当索引上溢出时返回尾时间点标记符号
     * 5. 当索引下溢出时返回起始时间点标记符号
     *
     * @param index 时间点索引
     * @return 指定位置的时间点标记符
     */
    public String tag(int index) {
        if (index < 0) {
            index = timePointList.size() + index;
        }
        return timePoint(index).tag;
    }

    /**
     * 获取运行总耗时
     *
     * @return 运行总耗时
     */
    public long spendTotal() {
        markEndIfNeed();
        return spendBetween(START_TAG, END_TAG);
    }

    /**
     * 获取上一个时间点至起始时间点之间的运行总耗时
     *
     * @return 上一个时间点至起始时间点之间的运行总耗时
     */
    public long lastSpend() {
        return spendBetween(START_TAG, lastMarkTag());
    }

    /**
     * 获取运行总耗时
     *
     * @return 运行总耗时
     */
    public long lastSpend(int preDistance) {
        if (preDistance >= 0) {
            return -1L;
        }
        return spendWithDistance(lastMarkTag(), preDistance);
    }

    /**
     * 获取起始时间点至传入时间点索引所在时间点之间的耗时
     *
     * @param index 时间点索引
     * @return 起始时间点至传入时间点索引所在时间点之间的耗时，返回值非负
     */
    public long spendFromStart(int index) {
        return spendBetween(0, index);
    }

    /**
     * 获取起始时间点至传入时间标记符号所在时间点之间的耗时
     *
     * @param endTag 时间标记符号
     * @return 起始时间点至传入时间标记符号所在时间点之间的耗时，返回值非负
     */
    public long spendFromStart(String endTag) {
        return spendBetween(START_TAG, endTag);
    }

    /**
     * 获取传入时间点索引所在时间点与距离该时间点指定距离的时间点之间的耗时
     *
     * @param index    时间点索引
     * @param distance 距离，往前为负，往后为正
     * @return 传入时间点索引所在时间点与距离该时间点指定距离的时间点之间的耗时；
     * 若距离该时间点指定距离的时间点不存在，上溢出时返回使用尾时间点，下溢出时使用起始时间点
     */
    public long spendWithDistance(int index, int distance) {
        return spendBetween(timePoint(index), timePoint(index + distance));
    }

    /**
     * 获取传入时间标记符号所在时间点与往前gap步所在时间点之间的耗时
     *
     * @param tag      时间点标记符号
     * @param distance 距离，往前为负，往后为正
     * @return 传入时间标记符号所在时间点与往前gap步所在时间点之间的耗时；
     * 若gap为非正数或往前gap步超出起始时间点，则返回起始时间点至传入时间标记符号所在时间点之间的耗时；
     * 返回值非负
     */
    public long spendWithDistance(String tag, int distance) {
        TimePoint end = timePointMap.get(tag);
        if (null != end) {
            return spendBetween(timePoint(end.index + distance), end);
        } else {
            return -1L;
        }
    }

    /**
     * 获取两个时间点索引所在时间点之间的耗时
     *
     * @param startIndex 起始时间点索引
     * @param endIndex   结束时间点索引
     * @return 获取两个时间点索引所在时间点之间的耗时，返回值非负
     */
    public long spendBetween(int startIndex, int endIndex) {
        return spendBetween(timePoint(startIndex), timePoint(endIndex));
    }

    /**
     * 获取两个时间标记符号所在时间点之间的耗时
     *
     * @param startTag 起始时间标记
     * @param endTag   结束时间标记
     * @return 两个时间标记符号所在时间点之间的耗时，返回值非负
     */
    public long spendBetween(String startTag, String endTag) {
        return spendBetween(timePointMap.get(startTag), timePointMap.get(endTag));
    }

    private long spendBetween(TimePoint start, TimePoint end) {
        if (null != end && null != start) {
            return Math.abs(end.time - start.time);
        } else {
            return -1L;
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        List<TimePoint> timePointList = new ArrayList<>(this.timePointList);
        TimePoint pre = timePointList.get(0);
        TimePoint cur = null;
        for (int index = 1; index < timePointList.size(); ++index) {
            cur = timePointList.get(index);
            builder.append(trimTagPrefix(pre.tag))
                    .append(" => ")
                    .append(trimTagPrefix(cur.tag))
                    .append(" : ")
                    .append(spendBetween(pre, cur))
                    .append(", ");
            pre = cur;
        }
        if (null == cur) {
            builder.append("startTime = ").append(pre.time);
        } else {
            builder.delete(builder.length() - 2, builder.length());
        }
        return builder.toString();
    }

    private String nextTag() {
        return TAG_PREFIX + "point_" + tagSeq.getAndIncrement();
    }

    private void markEndIfNeed() {
        if (!timePointMap.containsKey(END_TAG)) {
            markInternal(END_TAG);
        }
    }

    private TimePoint timePoint(int index) {
        index = Math.min(index, timePointList.size() - 1);
        index = Math.max(0, index);
        return timePointList.get(index);
    }

    private void markInternal(String tag) {
        TimePoint markTimePoint = TimePoint.newInstance(tag, timePointList.size());
        timePointList.add(markTimePoint);
        timePointMap.put(markTimePoint.tag, markTimePoint);
    }

    private boolean nonEmpty(String str) {
        return null != str && str.length() > 0;
    }

    private String trimTagPrefix(String tag) {
        if (nonEmpty(tag) && tag.startsWith(TAG_PREFIX)) {
            return tag.substring(TAG_PREFIX.length());
        }
        return tag;
    }

    /**
     * 计时点对象
     */
    private static class TimePoint {

        private long time = System.currentTimeMillis();

        private String tag;

        private int index;

        private static TimePoint newInstance(String tag, int index) {
            return new TimePoint(tag, index);
        }

        private TimePoint(String tag, int index) {
            this.tag = tag;
            this.index = index;
        }
    }

    public interface StopWatchCallback {
        void runtime(long time);
    }

    public static void main(String[] args) throws InterruptedException {
        StopWatch stopWatch = StopWatch.newInstance();

        Thread.sleep(1_000);
        stopWatch.mark();
        String tag0 = stopWatch.lastMarkTag();
        System.out.println("StopWatch.main() : " + tag0);
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend());

        System.out.println("");

        Thread.sleep(1_000);
        String tag1 = stopWatch.mark();
        System.out.println("StopWatch.main() : " + tag1);
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend());
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend(-1));
        System.out.println("StopWatch.main() : " + stopWatch.spendFromStart(stopWatch.tag(-1)));
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(tag1, -1));
        System.out.println("StopWatch.main() : " + stopWatch.spendBetween(tag1, tag0));
        System.out.println("StopWatch.main() : " + stopWatch.spendBetween(tag0, tag1));

        System.out.println("");

        Thread.sleep(1_000);
        String tag2 = "tag2";
        stopWatch.mark(tag2);
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend());
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend(-1));
        System.out.println("StopWatch.main() : " + stopWatch.spendFromStart(stopWatch.tag(-1)));
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(tag2, -1));
        System.out.println("StopWatch.main() : " + stopWatch.spendBetween(tag2, tag1));
        System.out.println("StopWatch.main() : " + stopWatch.spendBetween(tag2, tag0));

        System.out.println("");

        Thread.sleep(1_000);
        for (int i = 0; i < 1000; ++i) {
            stopWatch.mark();
        }
        System.out.println("StopWatch.main() : " + stopWatch.lastSpend());
        System.out.println("StopWatch.main() : " + stopWatch.spendFromStart(50));
        System.out.println("StopWatch.main() : " + stopWatch.spendBetween(2, 60));
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(60, -59));
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(60, -60));
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(60, -70));

        System.out.println("");

        Thread.sleep(1_000);
        System.out.println("StopWatch.main() : " + stopWatch.spendTotal());
        System.out.println("StopWatch.main() : " + stopWatch.spendWithDistance(60, 10000));

        System.out.println("");

        System.out.println("StopWatch.main() : " + stopWatch);
    }
}
